<?php

namespace Aoe\Emulator\Processor;

use Aoe\Emulator\Ifc\Type;
use Aoe\Emulator\Schema\Model;
use Aoe\Util\Bus\Convertor;
use Aoe\Util\Bus\Store\Obj;
use Aoe\Util\Bus\Store\Folder;
use Aoe\Util\Exception;
use Aoe\Util\File;
use Aoe\Util\Provider\Provider;
use Closure;

/**
 * # Json 文本数据库
 *  * 主键可以是 int 也可以是 string
 *
 * @extends Provider<Model, Texture>
 */
class Texture extends Provider
{
    const string ERR_FILE_NOT_FOUND = '定义文件查找失败';
    const string CHILDREN = 'children';
    const string AT_PID   = '@pid';
    const string AT_PS    = '@ps';
    const string AT_CS    = '@cs';

    const array inner_keys = [self::CHILDREN, self::AT_PID, self::AT_PS, self::AT_CS];
    
    /**
     * @throws \Exception
     */
    public function save($value): bool
    {
        if (!$value) return true;
        return Obj::getInstance()->set($value, $this->_file_name());
    }
    
    /**
     * @throws \Exception
     */
    public function load(?Closure $closure = null): false | string
    {
        $from = $this->_file_name();
        if (!is_file($from)) throw new Exception(self::ERR_FILE_NOT_FOUND);
        
        if (!$closure) return File::getString($from);

        $folder = Folder::getInstance();
        $sync = new Convertor(
            $folder,
            $folder,
            $closure,
            fn () => $this->_file_name(true)
        );
        
        return $sync->get($from);
    }

    /**
     * ## 指定ID获取信息
     *
     * @param string|int|string[] | int[] $ids
     * @param string|null $column
     * @return array
     * @throws \Exception
     */
    public function getRowsByIds(int | string | array $ids, ?string $column = null): array
    {
        $rows = $this->_catch_rows();
        if (empty($rows)) return [];

        $rs = is_scalar($ids) ?
            $rows[$ids] :
            array_filter($rows, fn ($k) => in_array($k, $ids), ARRAY_FILTER_USE_KEY);

        return $column ? array_column($rs, $column) : $rs;
    }
    
    /**
     * ## 创建选项
     *
     * @param int|string $id
     *
     * @return array
     */
    public function asOptions(int | string $id = 0): array
    {
        $obj = $this->_tree_obj();
        if (empty($obj)) return [];
        // 全部
        if (!static::ValidKey($id)) return $this->_map_options($obj);
        // 查询
        $item = $this->_find_item($obj, $id);
        return $item ? $this->_map_options($item) : [];
    }
    
    private function _map_options(array $value): array
    {
        $name = $this->element->getKey();
        $fn = function ($o) use ($name, &$fn) {
            $r = [ $name => $o[$name], Type::ID => $o[Type::ID] ];
            if (!isset($o[self::CHILDREN])) return $r;
            
            $r[self::CHILDREN] = array_map($fn, $o[self::CHILDREN]);
            return $r;
        };
        
        return array_map($fn, $value);
    }
    
    private function _find_item(array $vs, $id): array | false
    {
        foreach ($vs as $o) {
            if ($o[Type::ID] === $id) return $o;
            
            if (!isset($o[self::CHILDREN])) continue;
            
            $r = $this->_find_item($o[self::CHILDREN], $id);
            if ($r) return $r;
        }
        return false;
    }

    /**
     * @throws \Exception
     */
    private function _catch_rows()
    {
        $from = $this->_file_name();
        if (!is_file($from)) throw new Exception(self::ERR_FILE_NOT_FOUND);

        $obj = Obj::getInstance();
        $sync = new Convertor(
            $obj,
            $obj,
            fn (array $json) => $this->_json_to_Rows($json)[0],
            fn () => $this->_file_name(true, 'rows_')
        );

        return $sync->get($from);
    }
    
    /**
     * @return array
     */
    public function _tree_obj(): array
    {
        $from = $this->_file_name();
        return is_file($from) ? Obj::getInstance()->get($from) : [];
    }

    protected static function create_object($element): static
    {
        return new static($element);
    }

    private function _file_name(bool $md5 = false, string $prefix = ''): string | false
    {
        $file = strtolower($this->element->getTableName());
        if (!$md5) return safe_file(__DATA_DIR__, $file, '.json');

        $file = $prefix . md5($file);
        return safe_file(__TEMP__, $file, '.json');
    }

    /**
     * ## 将 Json 格式的数组转换为 rows 格式
     *
     * @param array $json
     * @param array|null $p
     * @return array
     */
    private function _json_to_Rows(array $json, ?array $p = null): array
    {
        /**
         * 全部节点信息
         */
        $rows = [];
        /**
         * 全部直系子节点
         */
        $ch   = [];
        /**
         * 全部子节点
         */
        $all = [];

        // 为了方便添加子元素，使用了foreach代替map
        foreach ($json as $v) {
            // 没有 ID
            if (!isset($v[Type::ID])) continue;
            // ID无效
            $id = $v[Type::ID];
            if (!static::ValidKey($id)) continue;
            
            $ch[]  = $id;
            $all[] = $id;
            
            // 本体数据截取
            $row = array_filter(
                $v,
                fn ($v, $k) => !in_array($k, self::inner_keys) and is_scalar($v),
                ARRAY_FILTER_USE_BOTH
            );
            
            // 父节点
            if ($p) {
                $pid = $p[Type::ID];
                $row[self::AT_PS] = [ ...($p[self::AT_PS] ?? []), $pid];
                $row[self::AT_PID] = $pid;
            }
            
            // 子节点列表包含自己
            if (!isset($v[self::CHILDREN])) {
                $row[self::AT_CS] = [$id];
                $rows[$id] = $row;
                continue;
            }

            // 子节点
            [$rs, $ids, $a] = $this->_json_to_Rows($v[self::CHILDREN], $row);
            $row[self::CHILDREN] = $ids;
            $row[self::AT_CS] = [...$a, $id];
            $all = [...$all, ...$a];

            $rows[$id] = $row;
            $rows = $rows + $rs;
        }

        return [$rows, $ch, $all];
    }

    public static function ValidKey($key): bool
    {
        return (is_int($key) && $key > 0) || (is_string($key) && strlen($key) > 0);
    }
}